#!/usr/bin/env python3

# Copyright 2017 The Imaging Source Europe GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sys
import signal
import argparse

from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel,
                             QDockWidget, QAction, QComboBox, QSizePolicy)

from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QIcon, QKeySequence

from tcam_capture.TcamDevice import TcamDevice, TcamDeviceIndex
from tcam_capture.DeviceDialog import DeviceDialog
from tcam_capture.TcamSignal import TcamSignals
from tcam_capture.TcamCaptureData import TcamCaptureData
from tcam_capture.PropertyDialog import PropertyDialog
from tcam_capture.TcamView import TcamView
from tcam_capture.OptionsDialog import OptionsDialog
from tcam_capture.ImageProvider import ImageProvider
from tcam_capture.Settings import Settings
from tcam_capture.Cache import Cache

import gi

gi.require_version("Gst", "1.0")

from gi.repository import Gst

import logging

log = logging.getLogger(__name__)


class TcamComboBox(QComboBox):
    """
    Extension of QComboBox
    Adds default entry
    This is used to indicate that a non-set value
    """
    def __init__(self, parent=None, default: str = None):
        super(TcamComboBox, self).__init__(parent)
        self.default_entry = default
        self.default_present = True

    def reset(self):
        """
        Remove all entries and re-add the default one
        """
        self.clear()
        self.addItem(self.default_entry)
        self.default_present = True

    def remove_default(self):
        """
        Remove the default entry from the combobox
        """
        if self.default_present:
            self.removeItem(0)
            self.default_present = False

    def is_default(self, entry: str):
        """
        return true if given entry is equal to the default one
        """
        if entry == self.default_entry:
            return True
        return False


class FmtSelection:
    """
    Helper class to keep track of different format selections
    """
    def __init__(self):
        self.fmt = None
        self.resolution = None
        self.fps = None

    def __eq__(self, other):

        return (self.fmt == other.fmt and
                self.resolution == other.resolution and
                self.fps == other.fps)


def split_serial(serial: str):
    """
    Split the given serial string when discovering '-'
    Returns serial and type
    """

    if "-" in serial:
        return serial.split("-", 1)
    else:
        return serial


class TCamCapture(QMainWindow):
    """
    Main application window for tcam-capture
    """
    open_options = pyqtSignal()

    def __init__(self, app, serial=None, caps_str=None, fullscreen=False):
        super().__init__()
        app.aboutToQuit.connect(self.quit)
        self.app = app

        # support <serial> and <serial>-<type>
        if serial:
            s = split_serial(serial)
            if type(s) is str:
                self.serial = serial
                self.device_type = ""
            else:
                self.serial = s[0]
                self.device_type = s[1]
        else:
            self.serial = serial
            self.device_type = ""
        self.caps_desc = None
        self.selected_format = FmtSelection()
        self.active_format = FmtSelection()

        self.settings = Settings()
        self.settings.load()

        self.cache = Cache()
        self.cache.load()

        self.data = TcamCaptureData()
        self.data.signals = TcamSignals()

        self.device_list = None  # needed for setting window title later on
        self.props = None  # property dialog docker widget
        self.props_widget = None  # property dialog widget
        self.view = None

        self.setWindowTitle("TCam Capture")
        image_provider = ImageProvider()
        self.setWindowIcon(image_provider.get_tcam_logo())

        self.setMinimumSize(960, 720)

        self.__setup_ui()
        self.indexer = TcamDeviceIndex()
        self.indexer.update_device_list.connect(self.update_device_list)

        self.indexer.create_device_list()  # call manually once to speed up first listing
        self.data.work_thread = QtCore.QThread()

        self.indexer.moveToThread(self.data.work_thread)
        self.data.work_thread.start()

        self.show()

        if (not self.serial and
                self.settings.reopen_device_on_startup and
                self.cache.last_serial):
            dev_list = self.indexer.device_list
            serial = self.cache.last_serial
            dev_type = self.cache.last_device_type
            # check if device exists
            if any(x.serial == serial and x.device_type == dev_type for x in dev_list):
                self.serial = serial
                self.device_type = dev_type

                if not caps_str and self.cache.last_format:
                    caps_str = self.cache.last_format

        if not self.serial and self.settings.show_device_dialog_on_startup:
            res, device = DeviceDialog.get_device(self.indexer)
            if res and device:
                self.serial = device.serial
                self.device_type = device.device_type

        if self.serial:
            self.open_device(self.serial, self.device_type, caps_str)

        if self.view and fullscreen:
            self.view.toggle_fullscreen()
            self.view.fit_view()
        self.get_focus()

    def __del__(self):
        if self.data.work_thread:
            self.data.work_thread.quit()

    def get_focus(self):
        """
        Give the mainwindow the focus
        """

        self.activateWindow()
        self.setFocus()

    def toggle_fullscreen(self):
        """
        Toggle fullscreen display of live stream
        """
        if self.view is not None:
            self.view.toggle_fullscreen()
        else:
            if self.isFullScreen():
                self.showNormal()
            else:
                self.showFullScreen()

    def toggle_maximized(self):
        """
        toggle if window is maximized
        """
        if self.isMaximized():
            self.showNormal()
        else:
            self.showMaximized()

    def keyPressEvent(self, event):
        """
        Overwrite of QMainWindow::keyPressEvent
        """

        value = event.key()

        bad_values = [QtCore.Qt.Key_Alt,
                      QtCore.Qt.Key_Control,
                      QtCore.Qt.Key_Shift,
                      QtCore.Qt.Key_Meta,
                      QtCore.Qt.Key_AltGr]

        if value in bad_values:
            return

        if event.modifiers() & QtCore.Qt.AltModifier:
            value += QtCore.Qt.ALT
        if event.modifiers() & QtCore.Qt.ControlModifier:
            value += QtCore.Qt.CTRL
        if event.modifiers() & QtCore.Qt.MetaModifier:
            value += QtCore.Qt.META
        if event.modifiers() & QtCore.Qt.ShiftModifier:
            value += QtCore.Qt.SHIFT

        log.info("Keypress {}".format(QKeySequence(value).toString(QKeySequence.NativeText)))

        event_sequence = QKeySequence(value)

        # handle special keys that should not be changeable
        if event.key() == QtCore.Qt.Key_F11:
            log.info("Toggling fullscreen")
            self.toggle_fullscreen()
            return
        elif event.key() == QtCore.Qt.Key_F10:
            log.info("Toggling maximize")
            self.toggle_maximized()
            return
        # handle user defined keybindings
        self.handle_keysequence(event_sequence)

    def handle_keysequence(self, keys: QKeySequence):

        if keys.toString() == self.settings.keybinding_fullscreen:
            self.toggle_fullscreen()
            return
        elif keys.toString() == self.settings.keybinding_open_dialog:
            self.open_device_dialog()
            return
        elif keys.toString() == self.settings.keybinding_save_image:
            self.save_image_action()
            return
        elif keys.toString() == self.settings.keybinding_trigger_image:
            self.trigger_image()
            return
        # log.debug("No action mapped to '{}'".format(keys.toString()))
        # log.debug("{}".format(self.settings.keybinding_trigger_image))

    def trigger_image(self):

        if self.view:
            self.view.trigger_image()

    def saved_image(self, image_path: str):
        """
        Slot for saving an image
        """

        self.statusBar().showMessage("Saved image {}".format(image_path), 2000)

    def __setup_ui(self):
        """
        General GUI setup code
        """
        self.pixel_label = QLabel("", self)
        self.pixel_label.setFixedWidth(100)
        self.pixel_coords_label = QLabel("", self)
        self.statusBar().addPermanentWidget(self.pixel_coords_label)
        self.statusBar().addPermanentWidget(self.pixel_label)

        self.current_fps_label = QLabel("", self)
        self.statusBar().addPermanentWidget(self.current_fps_label)

        self.toolbar = self.addToolBar("default")
        self.toolbar.setMovable(False)
        self.setContextMenuPolicy(Qt.NoContextMenu)

        exit_act = QAction(QIcon.fromTheme('exit'), 'Exit', self)
        exit_act.setShortcut('Ctrl+Q')
        exit_act.setStatusTip("Exit application")
        exit_act.triggered.connect(self.app.quit)
        self.toolbar.addAction(exit_act)

        preferences_action = QAction(QIcon.fromTheme("preferences-desktop"),
                                     "Preferences", self)
        preferences_action.setStatusTip("Open preferences dialog")
        preferences_action.triggered.connect(self.open_preferences)
        self.toolbar.addAction(preferences_action)

        self.device_label = QLabel("Device:")
        self.device_combo = QComboBox(self)
        self.device_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        # self.device_combo.setMinimumWidth(300)
        self.device_combo.activated[str].connect(self.on_device_selected)
        self.toolbar.addWidget(self.device_label)
        self.toolbar.addWidget(self.device_combo)

        self.format_label = QLabel("Format:")
        self.format_combo = QComboBox(self)
        self.format_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.format_combo.setMinimumWidth(150)
        self.format_combo.activated[str].connect(self.on_format_selected)
        self.toolbar.addWidget(self.format_label)
        self.toolbar.addWidget(self.format_combo)

        self.resolution_label = QLabel("Resolution:")
        self.resolution_combo = TcamComboBox(self, "Select Resolution")
        self.resolution_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.resolution_combo.activated[str].connect(self.on_resolution_selected)
        self.toolbar.addWidget(self.resolution_label)
        self.toolbar.addWidget(self.resolution_combo)

        self.fps_label = QLabel("FPS:")
        self.fps_combo = TcamComboBox(self, "Select FPS:")
        self.fps_combo.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.fps_combo.activated[str].connect(self.on_fps_selected)
        self.toolbar.addWidget(self.fps_label)
        self.toolbar.addWidget(self.fps_combo)

        self.save_image = QAction("Save Image", self)
        self.save_image.setIcon(QIcon.fromTheme("insert-image"))

        self.save_image.triggered.connect(self.save_image_action)
        self.toolbar.addAction(self.save_image)

        self.props_action = QAction("", self)
        self.props_action.setText("Properties")
        self.props_action.setVisible(False)
        self.props_action.triggered.connect(self.toggle_properties_dialog)
        self.toolbar.addAction(self.props_action)

        self.recording_action = QAction("", self)
        self.recording_action.setIcon(QIcon.fromTheme("media-record"))
        self.recording_action.setIconText("Start recording")
        self.recording_action.setText("Start recording")
        self.recording_action.triggered.connect(self.start_recording_video)
        self.toolbar.addAction(self.recording_action)

        self.set_device_menus_enabled(False)

        self.view = None

    def open_preferences(self):
        """
        SLOT for open_preferences button
        """
        result = OptionsDialog.get_options(self.settings,
                                           TcamView.has_dutils())

        if result:
            log.info("Saving settings")
        else:
            log.info("Settings not saved")

        if self.view:
            self.view.set_settings(self.settings)

        self.get_focus()

    def open_device_dialog(self):
        """
        SLOT for opening the device_dialog
        """
        res, device = DeviceDialog.get_device(self.indexer)
        if res and device:
            self.serial = device.serial
            self.device_type = device.device_type
            if self.serial:
                caps_str = None
                self.open_device(self.serial, self.device_type, caps_str)

    def save_image_action(self):
        """
        SLOT for save_image button
        Signal TcamView to save an image.
        """
        self.view.save_image(self.settings.get_image_type())

    def start_recording_video(self):
        """
        SLOT for triggering TcamView recording start
        """
        self.view.start_recording_video(self.settings.get_video_type())
        self.recording_action.setIcon(QIcon.fromTheme("media-playback-stop"))
        self.recording_action.setText("Stop recording")
        self.recording_action.triggered.disconnect(self.start_recording_video)
        self.recording_action.triggered.connect(self.stop_recording_video)

    def stop_recording_video(self):
        """
        SLOT for triggering TcamView recording stop
        """
        self.view.stop_recording_video()
        self.recording_action.setText("Start recording")
        self.recording_action.setIcon(QIcon.fromTheme("media-record"))
        # self.recording_action.clicked.connect(self.start_recording_video)
        self.recording_action.triggered.connect(self.start_recording_video)
        self.recording_action.triggered.disconnect(self.stop_recording_video)

    def update_device_list(self, device_list):
        """
        SLOT for indexer.update_device_list
        Refills the device combobox
        """
        self.device_list = device_list

        self.device_combo.clear()

        if not device_list:
            return

        self.device_combo.addItem("")

        active_entry = None

        for dev in device_list:

            action_string = "{model:<18} - {contype:<7} - {serial}".format(model=dev.model,
                                                                           contype=dev.device_type,
                                                                           serial=dev.serial)
            if (dev.serial == self.serial
                    and (self.device_type == ""
                         or dev.device_type.lower() == self.device_type.lower())):
                active_entry = action_string
            self.device_combo.addItem(action_string)

        if active_entry is not None:
            self.device_combo.setCurrentText(active_entry)

    def populate_fmt_box(self):
        """
        Fill format combobox with the correct values
        """
        if not self.caps_desc:
            log.error("No caps description available")
            return

        self.format_combo.clear()

        self.format_combo.addItems(self.caps_desc.get_fmt_list())

    def populate_resolution_box(self, fmt: str):
        """
        Fill resolution combobox with the correct values
        """
        self.resolution_combo.reset()
        self.fps_combo.reset()
        res_list = self.caps_desc.get_resolution_list(fmt)

        if not res_list:
            log.error("Could not retrieve resolutions for format '{}'. "
                      "Resolution box will be empty.".format(fmt))
            return

        self.resolution_combo.addItems(res_list)

    def populate_fps_box(self, fmt: str, resolution: str):
        """
        Fill fps combobox with the correct values
        """
        self.fps_combo.reset()
        self.selected_format.resolution = resolution

        fps_list = self.caps_desc.get_fps_list(fmt, resolution)

        if fps_list:
            self.fps_combo.addItems(fps_list)
        else:
            log.error("fps list is empty! fmt: '{}' resolution: '{}'".format(fmt,
                                                                             resolution))

    def new_pixel_under_mouse(self, active: bool,
                              mouse_x: int, mouse_y: int,
                              color: QtGui.QColor):
        """
        Slot for TcamView.new_pixel_under_mouse
        """
        if active:
            self.pixel_coords_label.setText("X:{: <4} Y:{: <4}".format(mouse_x,
                                                                       mouse_y))
            self.pixel_label.setText("{}".format(color.name()))

        else:
            self.pixel_coords_label.setText("")
            self.pixel_label.setText("")

    def on_format_selected(self, entry: str):
        """
        callback for self.format_combo
        """
        if (self.active_format.fmt == entry and
                self.selected_format.fmt == entry):
            # no needs to reset other menus,
            # since we already have the correct format
            return

        self.selected_format.fmt = entry
        self.selected_format.resolution = None
        self.selected_format.fps = None
        self.populate_resolution_box(entry)
        self.get_focus()

    def on_resolution_selected(self, entry: str):
        """
        callback for self.resolution_combo
        """
        if (self.active_format.resolution == entry and
                self.selected_format.resolution == entry):
            # no needs to reset the menu,
            # since we already have the correct resolution
            log.debug("Active format resolution is already selected")
            return

        if self.resolution_combo.is_default(entry):
            return

        self.resolution_combo.remove_default()
        self.selected_format.resolution = entry
        self.populate_fps_box(self.selected_format.fmt, entry)

    def on_fps_selected(self, entry: str):
        """callback for self.fps_combo"""
        log.info("on_fps_selected {}".format(entry))

        if self.fps_combo.is_default(entry):
            return
        self.selected_format.fps = entry

        if self.active_format == self.selected_format:
            # no needs to restart the stream,
            # since we already stream the correct format
            return

        self.fps_combo.remove_default()
        caps_str = self.caps_desc.generate_caps_string(self.selected_format.fmt,
                                                       self.selected_format.resolution,
                                                       self.selected_format.fps)

        log.info("User selected '{}'".format(caps_str))

        self.play(caps_str)

    def on_device_selected(self, entry: str):
        """callback for self.device_combo"""
        if entry == "":
            self.close()
            self.cache.last_serial = None
            self.cache.last_format = None
            self.cache.save()
            return

        e = entry.split(" - ")
        serial = e[2]
        dev_type = e[1].strip()  # ensure no whitespace remains
        self.open_device(serial, dev_type)

    def format_selected_callback(self, fmt: str, resolution: str, fps: str):
        """
        SLOT for TcamView::format_selected
        Used for setting GUI elements to correctly display caps information
        """
        self.selected_format.fmt = fmt
        self.selected_format.resolution = resolution
        self.selected_format.fps = fps

        self.active_format.fmt = fmt
        self.active_format.resolution = resolution
        self.active_format.fps = fps

        log.info("Selected format consists out of: '{}' - '{}' - '{}'".format(fmt, resolution, fps))
        self.populate_fmt_box()
        self.format_combo.setCurrentText(fmt)
        self.populate_resolution_box(fmt)
        # self.format_combo.blockSignals(True)
        self.resolution_combo.setCurrentText(resolution)
        # self.format_combo.blockSignals(False)

        self.populate_fps_box(fmt, resolution)

        self.fps_combo.setCurrentText(fps)

        self.cache.last_format = self.view.caps

    def open_device(self, serial: str, dev_type: str, caps_str: str = None):
        """Open device and starts video stream"""
        self.close()
        self.serial = serial
        self.device_type = dev_type

        if self.device_list:
            for dev in self.device_list:
                if (dev.serial == self.serial
                        and (self.device_type == ""
                             or dev.device_type == self.device_type)):
                    self.setWindowTitle("TCam Capture - {}({})".format(dev.model, serial))
                    # update device menu so that mark on opened camera is drawn
                    self.update_device_list(self.device_list)
                    break

        self.view = TcamView(self.serial, self.device_type, self)
        self.view.set_settings(self.settings)
        self.view.register_device_lost(self.lost_device)
        # create preemptive pipline to make tcambin available
        # needed for properties
        self.view.create_pipeline(caps_str)
        self.view.image_saved.connect(self.saved_image)
        self.view.new_pixel_under_mouse.connect(self.new_pixel_under_mouse)
        self.view.current_fps.connect(self.current_fps)
        self.view.format_selected.connect(self.format_selected_callback)
        self.view.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
                                QtWidgets.QSizePolicy.Expanding)
        self.setCentralWidget(self.view)
        self.caps_desc = self.view.get_caps_desc()
        self.data.tcam = self.view.get_tcam()
        self.view.pause()

        self.cache.last_serial = self.serial
        self.cache.last_device_type = self.device_type
        self.cache.save()
        state = self.cache.load_device_state()

        if state:
            self.view.load_state(state)

        self.props = QDockWidget("Properties")
        self.props_widget = PropertyDialog(self.data, self.view, self.props)
        self.props.setWidget(self.props_widget)
        self.props.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.props.setFloating(False)
        self.addDockWidget(Qt.LeftDockWidgetArea, self.props)
        self.set_device_menus_enabled(True)
        self.props_action.setVisible(True)

        self.play(caps_str)

    def current_fps(self, fps: float):
        """
        SLOT for TcamView.current_fps signal
        """
        self.current_fps_label.setText("FPS: {0:.2f}".format(fps))

    def play(self, video_format=None):
        """
        SLOT for activating a video stream
        """
        if self.view is not None:
            self.cache.last_format = video_format
            self.view.play(video_format)

    def stop(self):
        """
        Stops the video stream
        """
        if self.view is not None:
            state = self.view.get_state()

            self.cache.save_device_state(state)

            self.view.stop()

    def close(self):
        """
        Stops the video stream and closes the device
        """
        self.props_action.setVisible(False)

        self.setWindowTitle("TCam Capture")
        self.pixel_coords_label.setText("")
        self.pixel_label.setText("")
        self.current_fps_label.setText("")

        if self.props:
            self.props.setParent(None)
            self.props = None
            self.removeDockWidget(self.props)

        self.set_device_menus_enabled(False)
        self.setCentralWidget(None)
        self.serial = None

        if self.props_widget:
            self.props_widget.stop()
            self.props_widget = None

        if self.view is not None:
            self.stop()
            self.view.setParent(None)
            self.view = None
            # update menu to remove mark on open camera
            self.update_device_list(self.device_list)

        self.cache.last_format = None
        self.cache.last_serial = None
        self.cache.last_device_type = None

    def toggle_properties_dialog(self):
        """
        SLOT for toggle_properties_dialog button
        """
        if self.props:
            if self.props_action.isEnabled():
                if self.props.isVisible():
                    self.props.setVisible(False)
                else:
                    self.props.setVisible(True)

    def set_device_menus_enabled(self, enabled):
        """
        Convenience function to enable/disable device related GUI elements
        """
        # self.save_image.setEnabled(enabled)
        self.format_combo.setEnabled(enabled)
        self.resolution_combo.setEnabled(enabled)
        self.fps_combo.setEnabled(enabled)

        # self.recording_action.setEnabled(enabled)

    def quit(self):
        """
        Application shutdown function
        """
        log.info("Shutting down...")
        self.cache.save()
        self.hide()
        self.close()

        self.data.work_thread.quit()
        self.data.work_thread.wait()

    def lost_device(self):
        """
        Callback for device lost
        """
        log.error("Lost device. Stopping...")

        error_dialog = QtWidgets.QMessageBox(self)
        error_dialog.setIcon(QtWidgets.QMessageBox.Critical)
        error_dialog.setWindowTitle("Device lost")
        error_dialog.setText("Device has been lost. Please reconnect or re-open it.")

        error_dialog.show()

        self.close()


def reset_settings():
    """
    Reset the settings file to its default settings.
    """
    settings = Settings()
    settings.reset()
    settings.save()


def clear_cache():
    """
    Remove all cache files
    """
    cache = Cache()
    cache.reset()


def init():
    """
    This function performs additional setup steps that should happen
    independently of the actual application.
    argparse likes to exit the application when -h is appended
    This will cause exceptions, should QApplication already be running
    """

    parser = argparse.ArgumentParser(description='The Imaging Source Live Stream Application.')
    parser.add_argument("--serial", help="Open device with serial immediately",
                        action="store", dest="serial", default=None)
    parser.add_argument("--format", help="Open device with this gstreamer format",
                        action="store", dest="caps_str", default=None)
    parser.add_argument("--verbose", "-v", help="Increase logging level",
                        action="count", dest="verbose_count", default=0)
    parser.add_argument("--reset", help="Reset application settings and clear cache",
                        action="store_true")
    parser.add_argument("--fullscreen", help="Start to application with a fullscreen display",
                        action="store_true")
    # these are here to make argparse shut up about unknown arguments
    # we want this application to allow gstreamer logging
    parser.add_argument("--gst-debug", help=argparse.SUPPRESS)
    parser.add_argument("--gst-debug-level", help=argparse.SUPPRESS)
    parser.add_argument("--gst-debug-no-color", help=argparse.SUPPRESS,
                        action="store_true")  # required since no-color is a simple flag

    arguments = parser.parse_args()

    if arguments.reset:
        reset_settings()
        clear_cache()

    if arguments.verbose_count == 0:
        level = logging.ERROR
    elif arguments.verbose_count == 1:
        level = logging.WARNING
    elif arguments.verbose_count == 2:
        level = logging.INFO
    else:
        level = logging.DEBUG

    logging.basicConfig(format='[%(asctime)s:%(msecs)3d] [%(levelname)8s] [%(filename)20s:%(lineno)3d] - %(message)s',
                        datefmt='%Y-%m-%d:%H:%M:%S',
                        level=level)
    Gst.init(sys.argv)
    return arguments.serial, arguments.caps_str, arguments.fullscreen


if __name__ == '__main__':

    # initializing the application has the following priority of setting:
    #
    # commandline arguments overwrite
    # cache values, which overwrite
    # default values

    serial, caps_str, fullscreen = init()

    # keep the default signal handling
    # should the application hang itself, due to gstreamer "hick-ups"
    # we still have the possibility to kill it with ctrl-c
    signal.signal(signal.SIGINT, signal.SIG_DFL)

    app = QApplication(sys.argv)
    tcam_capture = TCamCapture(app, serial, caps_str, fullscreen)

    sys.exit(app.exec_())
